"""Build DAOS Control Plane"""
# pylint: disable=too-many-locals
import os
import socket
import subprocess  # nosec
from binascii import b2a_hex
from datetime import datetime, timezone
from os import urandom
from os.path import join


def is_firmware_mgmt_build(benv):
    "Check whether this build has firmware management enabled."
    return benv["FIRMWARE_MGMT"] == 1


def get_build_tags(benv):
    "Get custom go build tags."
    tags = ["spdk"]
    if is_firmware_mgmt_build(benv):
        tags.append("firmware")
    if not is_release_build(benv):
        tags.append("fault_injection")
        tags.append("pprof")
    else:
        tags.append("release")
    return f"-tags {','.join(tags)}"


def is_release_build(benv):
    "Check whether this build is for release."
    return benv.get("BUILD_TYPE") == "release"


def get_build_flags(benv):
    """Return string of build flags"""
    if is_release_build(benv):
        return '-buildmode=pie'
    if 'SANITIZERS' in benv and benv['SANITIZERS'] != "":
        return '-asan'
    # enable race detector for non-release builds
    return '-race'


def gen_build_id():
    """generate a unique build id per binary for use by RPM
       https://fedoraproject.org/wiki/PackagingDrafts/Go#Build_ID"""
    buildid = b2a_hex(urandom(20))
    return '0x' + buildid.decode()


def get_build_info():
    """Attempt to retrieve commit/tag details from the build environment."""
    try:
        cmd = subprocess.run(['git', 'describe', '--tags', '--dirty', '--always'],
                             stdout=subprocess.PIPE, check=True)  # nosec
    except (subprocess.CalledProcessError, FileNotFoundError):
        return ''
    return cmd.stdout.decode().strip()


def go_ldflags(benv):
    "Create the ldflags option for the Go build."

    build_host = ''
    if not is_release_build(benv):
        build_host = socket.getfqdn()
    build_time = datetime.now(timezone.utc).astimezone().isoformat()
    build_info = get_build_info()
    Import('daos_version', 'conf_dir')
    path = 'github.com/daos-stack/daos/src/control/build'
    return ' '.join([f'-X {path}.DaosVersion={daos_version}',
                     f'-X {path}.ConfigDir={conf_dir}',
                     f'-X {path}.BuildHost={build_host}',
                     # NB: dynamic values should be enclosed in $(...$)
                     # to avoid unnecessary rebuilds
                     f'-X $({path}.BuildInfo={build_info}$)',
                     f'-X $({path}.BuildTime={build_time}$)',
                     f'-B $({gen_build_id()}$)'])


def install_go_bin(env, name, libs=None, install_man=False):
    """
    Build a Go binary whose source is under directory 'name' and install it
    libs should be a list of scons-built libraries, or None if none are needed.
    """

    gosrc = Dir('.').srcnode().abspath
    sources = Glob(f'cmd/{name}/*.go')
    sources.append(env.d_go_bin)

    install_src = f'github.com/daos-stack/daos/src/control/cmd/{name}'
    build_bin = join('$BUILD_DIR/src/control', name)

    if libs is None:
        libs = []
    libs.extend(['daos_common', 'cart', 'gurt'])

    target = env.d_run_command(name, sources, libs,
                               f'cd {gosrc}; {env.d_go_bin} build -mod vendor '
                               + f'-ldflags "{go_ldflags(env)}" '
                               + f'{get_build_flags(env)} '
                               + f'{get_build_tags(env)} '
                               + f'-o {build_bin} {install_src}')
    env.Install('$PREFIX/bin', target)
    if install_man:
        gen_bin = join('$BUILD_DIR/src/control', name)
        build_path = join('$BUILD_DIR/src/control', f'{name}.8')
        menv = env.Clone()
        # This runs code from the build area so needs LD_LIBRARY_PATH set.
        menv.d_enable_ld_path(["cart", "gurt", "client/api", "common", "client/dfs", "utils",
                               "utils/self_test"])
        menv.Command(build_path, target, f'{gen_bin} manpage -o {build_path}')
        menv.Install('$PREFIX/share/man/man8', build_path)


def scons():
    """Execute build"""

    Import('env', 'prereqs')

    denv = env.Clone()

    if denv.get("COMPILER") == 'covc':
        denv.Replace(CC='gcc', CXX='g++')

    # if SPDK_PREFIX differs from PREFIX, copy dir so files can be accessed at runtime
    prefix = denv.subst("$PREFIX")
    sprefix = denv.subst("$SPDK_PREFIX")
    if sprefix not in ["", prefix]:
        def install_dir(srcdir):
            """walk a directory and install targets"""
            for root, _dirs, files in os.walk(srcdir):
                dest_root = os.path.join(prefix, root, sprefix)
                for fname in files:
                    denv.Install(dest_root, join(root, fname))

        install_dir('share/spdk/scripts')
        install_dir('include/spdk')

    denv.Tool('go_builder')

    # Sets CGO_LDFLAGS for rpath options
    denv.d_add_rpaths("..", True, True)
    denv.require('protobufc')
    denv.AppendENVPath("CGO_CFLAGS", denv.subst("$_CPPINCFLAGS"), sep=" ")
    if prereqs.client_requested():
        install_go_bin(denv, "daos_agent")
        install_go_bin(denv, "dmg", install_man=True)
        if prereqs.test_requested():
            install_go_bin(denv, "hello_drpc")

        dbenv = denv.Clone()
        dblibs = dbenv.subst("-L$BUILD_DIR/src/gurt "
                             "-L$BUILD_DIR/src/cart "
                             "-L$BUILD_DIR/src/common/dav_v2 "
                             "-L$BUILD_DIR/src/common "
                             "-L$BUILD_DIR/src/client/dfs "
                             "-L$BUILD_DIR/src/utils "
                             "-L$BUILD_DIR/src/utils/self_test "
                             "$_RPATH")
        dbenv.AppendENVPath("CGO_LDFLAGS", dblibs, sep=" ")
        install_go_bin(dbenv, 'daos', libs=['daos_cmd_hdlrs', 'dfs', 'duns', 'daos',
                                            'daos_self_test'],
                       install_man=True)

    if not prereqs.server_requested():
        return

    senv = denv.Clone()

    denv.AppendENVPath("CGO_CFLAGS", denv.subst("$_CPPINCFLAGS"), sep=" ")

    SConscript('lib/spdk/SConscript', exports='denv')

    denv.d_add_rpaths("..", True, True)

    # Copy setup_spdk.sh script to be executed at daos_server runtime.
    senv.Install('$PREFIX/share/daos/control', 'server/init/setup_spdk.sh')

    install_go_bin(senv, "daos_server")

    aenv = denv.Clone()
    aenv.require('spdk', 'pmdk', 'ofi', 'argobots')

    aenv.AppendUnique(LINKFLAGS=["-Wl,--no-as-needed"])
    aenv.Replace(RPATH=[])
    cgolibdirs = aenv.subst("-L$BUILD_DIR/src/control/lib/spdk "
                            "-L$BUILD_DIR/src/gurt "
                            "-L$BUILD_DIR/src/cart "
                            "-L$BUILD_DIR/src/common/dav_v2 "
                            "-L$BUILD_DIR/src/common "
                            "-L$BUILD_DIR/src/utils/ddb "
                            "-L$SPDK_PREFIX/lib "
                            "-L$OFI_PREFIX/lib $_RPATH")
    # Explicitly link RTE & SPDK libs for CGO access
    ldopts = cgolibdirs + " -lspdk_env_dpdk -lspdk_nvme -lspdk_vmd -lrte_mempool" + \
        " -lrte_mempool_ring -lrte_bus_pci -lnvme_control -lnuma -ldl"
    aenv.AppendENVPath("CGO_LDFLAGS", ldopts, sep=" ")

    aenv.AppendENVPath("CGO_CFLAGS", aenv.subst("$_CPPINCFLAGS"), sep=" ")

    # Sets CGO_LDFLAGS for rpath
    aenv.d_add_rpaths(None, True, True)
    install_go_bin(aenv, 'daos_server_helper', libs=['nvme_control'])

    if is_firmware_mgmt_build(aenv):
        print("(EXPERIMENTAL) Building DAOS firmware tools")
        install_go_bin(aenv, "daos_firmware_helper", libs=['nvme_control'])

    ddb_env = aenv.Clone()
    ddb_env.AppendUnique(RPATH_FULL=['$PREFIX/lib64/daos_srv'])
    ddb_env.d_add_rpaths(None, True, True)

    # Add vos and dependent libs for ddb
    ddb_env.AppendENVPath("CGO_LDFLAGS", " -lvos -ldav_v2 -ldaos_common_pmem -lpmem "
                                         "-labt -lgurt -luuid -lbio -lcart", sep=" ")
    install_go_bin(ddb_env, "ddb", ['ddb'])


if __name__ == "SCons.Script":
    scons()
